/*
 * Copyright (c) 2015, the DeltaSync# project. All Rights Reserved.
 * This project is a C# port of the JDeltaSync project:
 *     http://jdeltasync.googlecode.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.IO;

namespace PM.Net.DeltaSync.hu01 {
    /**
     * Decompresses data compressed using the proprietary <i>HU01</i> algorithm
     * (see <a href="http://en.wikipedia.org/wiki/DeltaSync">Wikipedia's DeltaSync page</a>
     * for info on the compression). This class resembles the {@link Inflater} class 
     * and is used in a similar manner.
     * <p>
     * Some of the JavaDoc comments have been copied from Apache Harmony's 
     * {@code java.util.zip.Inflater} code.
     */
    public class HU01Decompressor {
        private static int HU01_MAGIC = 0x31305548; // HU01 (LE)
        private static int SCBH_MAGIC = 0x48424353; // SCBH (LE)
        private static int TABLE_SIZE = 256;
    
        private bool         inHeader          = true;
        private long         decompressedSize  = 0;
        private long         decompressedBytes = 0;
        private BinaryWriter buffer;
        private BinaryWriter decoded;
        private short[]      table = new short[0x8000];
        
        /**
         * Creates a new instance using an initial buffer size of 4096 bytes.
         */
        public HU01Decompressor()
               : this(4096)
        {}
        
        /**
         * Creates a new instance using the specified initial buffer size.
         * 
         * @param initialBufferSize the initail buffer size.
         */
        public HU01Decompressor(int initialBufferSize) {
            buffer = new BinaryWriter(initialBufferSize);
        }
        
        public static void Decompress(Stream source, Stream destination) {
            var buffer    = new byte[0x4000];
            int len;
            
            var processor = new HU01Decompressor();
            while ((len = source.Read(buffer, 0, buffer.Length)) > 0) {
                processor.addInput(buffer, 0, len);
                
                len = processor.decompress(buffer, 0, buffer.Length);
                if (len > 0)
                    destination.Write(buffer, 0, len);
            }
            
            while ((len = processor.decompress(buffer, 0, buffer.Length)) > 0)
                destination.Write(buffer, 0, len);
            if (!processor.Finished)
                throw new HU01Exception("HU01 stream truncated");
            destination.Flush();
        }
        
        /**
         * Resets the {@code HU01Decompressor}. Should be called prior to inflating a new
         * set of data.
         */
        public void Reset() {
            inHeader          = true;
            decompressedSize  = 0;
            decompressedBytes = 0;
            buffer.Clear();
            if (decoded != null)
                decoded.Clear();
        }
        
        /**
         * Indicates if the {@code HU01Decompressor} has decompressed the entire compressed
         * stream. If compressed bytes remain this method will return {@code false}. This 
         * method should be called after all compressed input is supplied to the 
         * {@code HU01Decompressor}.
         *
         * @return {@code true} if all input has been decompressed, {@code false}
         *         otherwise.
         */
        public bool Finished {
            get { return !inHeader && decompressedBytes == decompressedSize; }
        }
        
        /**
         * Decompresses bytes from current input and stores them in {@code buf}.
         *
         * @param buf the buffer where decompressed data bytes are written.
         * @throws HU01Exception if the underlying stream is corrupted.
         * @return the number of bytes decompressed. Returns 0 if more data is needed.
         *         If 0 is returned {@link #addInput(byte[])} or {@link #addInput(byte[], int, int)}
         *         has to be called to provide more compressed data.
         */
        public int decompress(byte[] buf) {
            return decompress(buf, 0, buf.Length);
        }
        
        /**
         * Decompresses up to {@code nbytes} bytes from the current input and stores them in {@code
         * buf} starting at {@code off}.
         *
         * @param buf the buffer to write decompressed bytes to.
         * @param off the offset in buffer where to start writing decompressed data.
         * @param nbytes the number of decompressed bytes to write to {@code buf}.
         * @throws HU01Exception if the underlying stream is corrupted.
         * @return the number of bytes decompressed. Returns 0 if more data is needed.
         *         If 0 is returned {@link #addInput(byte[])} or {@link #addInput(byte[], int, int)}
         *         has to be called to provide more compressed data.
         */
        public int decompress(byte[] buf, int off, int nbytes) {
            if (Finished) {
                return -1;
            }
            
            if (inHeader) {
                long ret = header();
                if (ret < 0) {
                    return 0;
                }
                inHeader         = false;
                decompressedSize = ret;
            }
            
            if (decoded == null || decoded.Remaining == 0) {
                if (block() == 0) {
                    return 0;
                }
            }
            
            int n = decoded.Retrive(buf, off, nbytes);
            decompressedBytes += n;
            return n;
        }
        
        /**
         * Adds input to be decompressed. This method should be
         * called if {@link #decompress(byte[])} or 
         * {@link #decompress(byte[], int, int)} return 0.
         *
         * @param buf the input buffer.
         */
        public void addInput(byte[] buf) {
            addInput(buf, 0, buf.Length);
        }
    
        /**
         * Adds input to be decompressed. This method should be
         * called if {@link #decompress(byte[])} or 
         * {@link #decompress(byte[], int, int)} return 0.
         *
         * @param buf the input buffer.
         * @param off the offset to read from the input buffer.
         * @param nbytes the number of bytes to read.
         */
        public void addInput(byte[] buf, int off, int len) {
            buffer.Append(buf, off, len);
        }
        
        private long header() {
            BinaryAccess b = buffer.GetAccess(); // slice to prevent changing buffer until we now we've got the full header 
            if (b.Length < 36) {
                // We need at least 36 bytes
                return -1;
            }
            int magic = b.ReadInt(0);
            if (magic != HU01_MAGIC) {
                char[] chars = new char[] { (char) (magic & 0xff), (char) ((magic >> 8) & 0xff), (char) ((magic >> 16) & 0xff), (char) ((magic >> 24) & 0xff) };
                throw new HU01Exception("Bad header: 'HU01' expected at beginning of header (was " + new string(chars) + ")");
            }
            int headerSize = b.ReadInt(4);
            if (headerSize < 0x28) {
                throw new HU01Exception("Bad header: Header size must be at least 0x28 bytes (was 0x" + headerSize.ToString("X") + ")");
            }
            
            if (b.Length < headerSize) {
                return -1;
            }
            
            long size = b.ReadUInt(32);
            buffer.AdvancePosition((int)headerSize);
            return size;
        }
        
        private int block() {
            BinaryAccess b = buffer.GetAccess(); // slice to prevent changing buffer until we now we've got a full block 
            if (b.Length < 20) {
                // We need at least 20 bytes for the header
                return 0;
            }
            int magic = b.ReadInt(0);
            if (magic != SCBH_MAGIC) {
                char[] chars = new char[] {(char) (magic & 0xff), (char) ((magic >> 8) & 0xff), (char) ((magic >> 16) & 0xff), (char) ((magic >> 24) & 0xff)};
                throw new HU01Exception("Bad block header: 'SCBH' expected at beginning of block header (was " + new string(chars) + ")");
            }
            int  headerSize            = b.ReadInt(4);
            int  decompressedBlockSize = b.ReadInt(8);
            uint crc                   = b.ReadUInt(12);
            int  compressedBlockSize   = b.ReadInt(16);
            if (headerSize < 0 || decompressedBlockSize < 0 || compressedBlockSize < 0) {
                // We do not allow overflow size values
                throw new HU01Exception("Invalid HU01 header");
            }
            if (b.Length < headerSize + compressedBlockSize) {
                // We need at least headerSize+compressedBlockSize bytes for the entire block
                return 0;
            }
            
            if (decoded == null)
                decoded = new BinaryWriter(decompressedBlockSize);
            else if (decoded.Capacity < decompressedBlockSize) 
                decoded.Capacity = decompressedBlockSize;
            decoded.Clear();
            
            /// <Marian date="2015-04-17" note="I have changed the logic, the condition was originaly too restrictive..." />
            #if ORIGINAL_CODE_V0
            if ((compressedBlockSize == decompressedBlockSize) && decompressedBlockSize < 2048) {
            #else
            if ((compressedBlockSize == decompressedBlockSize) || // no compression... 
                (compressedBlockSize <= TABLE_SIZE)) {            // there is no data available, we cannot have only HeaderData
            #endif
                /*
                 * Block isn't compressed. Just copy the bytes. We don't know how to properly check for 
                 * uncompressed blocks. For now the check above seems to work.
                 */
                b.GetAccess(headerSize, compressedBlockSize).WriteTo(decoded);
            } else {
                try {
                    // Compressed block
                    if (!HU01.build_decompression_table(b.GetAccess(headerSize, TABLE_SIZE), table)) {
                        throw new HU01Exception("Bad block table");
                    }
                
                    decoded.Extend(decompressedBlockSize);
                    HU01.decompress_hu01_block(b.GetAccess(headerSize + TABLE_SIZE, compressedBlockSize - TABLE_SIZE), table, decoded.GetAccess());
                } catch (HU01Exception) {
                    throw;
                } catch (Exception ex) {
                    throw new HU01Exception(ex.Message, ex);
                }
            }
            
            buffer.AdvancePosition(headerSize + compressedBlockSize);
            buffer.Compact();
            
            CRC32 crc32 = new CRC32();
            crc32.Update(decoded.GetBuffer(), decoded.Position, decoded.Length);
            if (crc32.Value != crc) {
                throw new HU01Exception("CRC check failed for block. Expected " + crc.ToString("X") + ". Was " + crc32.Value.ToString("X") + ".");
            }
            
            return decompressedBlockSize;
        }
    }
}